package com.b.utils;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.Set;

import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.indices.GetIndexResponse;
import org.elasticsearch.xcontent.XContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.stereotype.Component;

import jakarta.annotation.Resource;
import org.reflections.Reflections;
import org.springframework.aop.support.AopUtils;

@Component
public class EsDataSyncUtil {
    private static final Logger logger = LoggerFactory.getLogger(EsDataSyncUtil.class);
    private static final int BATCH_SIZE = 1000;
    private static final int MAX_RETRIES = 3;
    private static final long RETRY_DELAY_MS = 1000;

    @Resource
    private RestHighLevelClient esClient;

    @Resource
    private ApplicationContext applicationContext;

    /**
     * 一键上传所有 domain 包下带 @Document 注解的实体类数据到ES（无论有无数据都创建索引，并打印所有被扫描到的实体类名称）
     * @throws Exception 同步异常
     */
    public void uploadAllEntitiesToEs() throws Exception {
        Reflections reflections = new Reflections("com.b.domain");
        Set<Class<?>> entityClasses = reflections.getTypesAnnotatedWith(Document.class);
        logger.info("扫描到的@Document实体类如下：");
        for (Class<?> entityClass : entityClasses) {
            logger.info(" - {}", entityClass.getName());
            String indexName = entityClass.getAnnotation(Document.class).indexName();
            // 先检查并创建索引
            if (!indexExists(indexName)) {
                createIndex(indexName);
            }
            // 尝试查找对应的ServiceImpl并调用getAll
            String serviceImplName = entityClass.getSimpleName() + "ServiceImpl";
            Object serviceImpl = null;
            for (String beanName : applicationContext.getBeanDefinitionNames()) {
                Object bean = applicationContext.getBean(beanName);
                Class<?> targetClass = AopUtils.getTargetClass(bean);
                if (targetClass.getSimpleName().equals(serviceImplName)) {
                    serviceImpl = bean;
                    break;
                }
            }
            if (serviceImpl == null) {
                logger.warn("未找到 {} 的ServiceImpl，跳过数据同步", entityClass.getSimpleName());
                continue;
            }
            try {
                Method getAllMethod = serviceImpl.getClass().getMethod("getAll");
                List<?> entities = (List<?>) getAllMethod.invoke(serviceImpl);
                if (entities == null || entities.isEmpty()) {
                    logger.info("{} 没有数据，但索引 {} 已创建", serviceImplName, indexName);
                    continue;
                }
                for (int i = 0; i < entities.size(); i += BATCH_SIZE) {
                    List<?> batch = entities.subList(i, Math.min(i + BATCH_SIZE, entities.size()));
                    bulkIndexDocuments(indexName, batch);
                    logger.info("已处理 {}/{} 条记录", Math.min(i + BATCH_SIZE, entities.size()), entities.size());
                }
                logger.info("{} 同步完成", entityClass.getName());
            } catch (NoSuchMethodException e) {
                logger.warn("{} 没有getAll方法，跳过数据同步", serviceImplName);
            } catch (Exception e) {
                logger.error("同步实体类 {} 失败: {}", entityClass.getName(), e.getMessage(), e);
            }
        }
    }

    /**
     * 一键删除ES中所有 domain 包下带 @Document 注解的实体类对应索引的数据
     * @throws IOException 删除异常
     */
    public void deleteAllEntitiesFromEs() throws IOException {
        Reflections reflections = new Reflections("com.b.domain");
        Set<Class<?>> entityClasses = reflections.getTypesAnnotatedWith(Document.class);
        logger.info("扫描到的@Document实体类如下：");
        for (Class<?> entityClass : entityClasses) {
            logger.info(" - {}", entityClass.getName());
            String indexName = entityClass.getAnnotation(Document.class).indexName();
            logger.info("准备删除索引 {} (实体类: {})", indexName, entityClass.getName());
            if (indexExists(indexName)) {
                DeleteIndexRequest request = new DeleteIndexRequest(indexName);
                esClient.indices().delete(request, RequestOptions.DEFAULT);
                logger.info("索引 {} 已成功删除", indexName);
            } else {
                logger.info("索引 {} 不存在，跳过删除", indexName);
            }
        }
    }

    /**
     * 一键删除ES中所有索引的数据（包括有数据的索引）
     * @throws IOException 删除异常
     */
    public void deleteAllIndicesFromEs() throws IOException {
        // 获取所有索引名
        GetIndexRequest request = new GetIndexRequest("*");
        GetIndexResponse response = esClient.indices().get(request, RequestOptions.DEFAULT);
        String[] allIndices = response.getIndices();
        for (String index : allIndices) {
            // 跳过系统索引（如.kibana等），只删业务索引
            if (!index.startsWith(".")) {
                DeleteIndexRequest deleteRequest = new DeleteIndexRequest(index);
                esClient.indices().delete(deleteRequest, RequestOptions.DEFAULT);
                logger.info("已删除索引: {}", index);
            }
        }
    }


    /**
     * 检查索引是否存在
     * @param indexName 索引名
     * @return 是否存在
     * @throws IOException 检查异常
     */
    private boolean indexExists(String indexName) throws IOException {
        GetIndexRequest request = new GetIndexRequest(indexName);
        return esClient.indices().exists(request, RequestOptions.DEFAULT);
    }

    /**
     * 创建索引
     * @param indexName 索引名
     * @throws IOException 创建异常
     */
    private void createIndex(String indexName) throws IOException {
        CreateIndexRequest request = new CreateIndexRequest(indexName);
        CreateIndexResponse response = esClient.indices().create(request, RequestOptions.DEFAULT);
        if (response.isAcknowledged()) {
            logger.info("成功创建索引 {}", indexName);
        } else {
            logger.error("创建索引 {} 失败", indexName);
            throw new IOException("创建索引失败");
        }
    }

    /**
     * 批量索引文档
     * @param indexName 索引名
     * @param documents 文档列表
     * @throws IOException 索引异常
     */
    public void bulkIndexDocuments(String indexName, List<?> documents) throws IOException {
        BulkRequest bulkRequest = new BulkRequest();
        for (Object document : documents) {
            // 将实体对象转换为Map
            Map<String, Object> source = convertObjectToMap(document);
            String idValue = getIdValue(document);
            if (idValue == null) {
                throw new RuntimeException("主键(@Id)不能为空");
            }
            IndexRequest indexRequest = new IndexRequest(indexName)
                .id(idValue)
                .source(source, XContentType.JSON);
            bulkRequest.add(indexRequest);
        }
        // 执行批量操作，带重试机制
        executeBulkRequestWithRetry(bulkRequest);
    }

    /**
     * 带重试机制的批量请求执行
     * @param bulkRequest 批量请求
     * @throws IOException 请求异常
     */
    private void executeBulkRequestWithRetry(BulkRequest bulkRequest) throws IOException {
        int retries = 0;
        while (true) {
            try {
                BulkResponse response = esClient.bulk(bulkRequest, RequestOptions.DEFAULT);
                if (response.hasFailures()) {
                    logger.error("批量操作失败: {}", response.buildFailureMessage());
                    throw new IOException("批量操作失败");
                }
                break;
            } catch (IOException e) {
                retries++;
                if (retries > MAX_RETRIES) {
                    logger.error("达到最大重试次数，批量操作失败", e);
                    throw e;
                }
                logger.warn("批量操作失败，尝试重试 ({}/{}): {}", retries, MAX_RETRIES, e.getMessage());
                try {
                    Thread.sleep(RETRY_DELAY_MS * retries);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new IOException("批量操作中断", ie);
                }
            }
        }
    }

    /**
     * 将对象转换为Map
     * @param object 对象
     * @return Map表示
     */
    private Map<String, Object> convertObjectToMap(Object object) {
        if (object == null) {
            return Collections.emptyMap();
        }
        if (object instanceof Map) {
            return (Map<String, Object>) object;
        }
        Map<String, Object> result = new HashMap<>();
        Class<?> clazz = object.getClass();
        for (Method method : clazz.getMethods()) {
            String methodName = method.getName();
            if (methodName.startsWith("get") && methodName.length() > 3 && method.getParameterCount() == 0) {
                try {
                    String fieldName = methodName.substring(3);
                    fieldName = Character.toLowerCase(fieldName.charAt(0)) + fieldName.substring(1);
                    Object value = method.invoke(object);
                    // 日期类型统一转为ISO8601带毫秒和时区
                    if (value instanceof java.util.Date date) {
                        // java.util.Date和java.sql.Date都走这里
                        value = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(
                            date.toInstant().atZone(java.time.ZoneOffset.UTC)
                        );
                    } else if (value instanceof java.time.LocalDateTime ldt) {
                        value = ldt.atZone(java.time.ZoneId.systemDefault()).withZoneSameInstant(java.time.ZoneOffset.UTC).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"));
                    } else if (value instanceof java.time.LocalDate ld) {
                        value = ld.atStartOfDay(java.time.ZoneId.systemDefault()).withZoneSameInstant(java.time.ZoneOffset.UTC).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"));
                    }
                    result.put(fieldName, value);
                } catch (Exception e) {
                    logger.warn("获取字段值失败: {}", e.getMessage());
                }
            }
            if (methodName.startsWith("is") && methodName.length() > 2 && 
                method.getParameterCount() == 0 && 
                (Boolean.class.equals(method.getReturnType()) || boolean.class.equals(method.getReturnType()))) {
                try {
                    String fieldName = methodName.substring(2);
                    fieldName = Character.toLowerCase(fieldName.charAt(0)) + fieldName.substring(1);
                    Object value = method.invoke(object);
                    result.put(fieldName, value);
                } catch (Exception e) {
                    logger.warn("获取字段值失败: {}", e.getMessage());
                }
            }
        }
        return result;
    }

    private String getIdValue(Object document) {
        // 1. 先找字段上的@Id
        for (Field field : document.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(Id.class)) {
                field.setAccessible(true);
                try {
                    Object value = field.get(document);
                    return value != null ? value.toString() : null;
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("获取@Id字段值失败", e);
                }
            }
        }
        // 2. 再找getter上的@Id
        for (Method method : document.getClass().getMethods()) {
            if (method.isAnnotationPresent(Id.class) && method.getParameterCount() == 0) {
                try {
                    Object value = method.invoke(document);
                    return value != null ? value.toString() : null;
                } catch (Exception e) {
                    throw new RuntimeException("获取@Id方法值失败", e);
                }
            }
        }
        throw new RuntimeException("未找到@Id主键字段或方法");
    }
}